package com.example.servicedemo;

import java.util.ArrayList;
import java.util.Calendar;

import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class WorkoutDatabaseHelper extends SQLiteOpenHelper {
	
	private static final int MILLISEC_TO_HOURS = 3600000;
	
	private static final String TAG = "WorkoutDatabaseHelper";
	
	/*****************************************************************
	 * Shared Preferences definitions for Workout Session
	 *****************************************************************/
	private static final String SP_WORKOUT_SESSION = "CycleCoach_activeWorkout";
	private static final String SP_WORKOUT_SESSION_ID = "CycleCoach_activeWorkout_sessionId";
	private static final String SP_WORKOUT_SESSION_INDEX = "CycleCoach_activeWorkout_sessionIndex";
	private static final String SP_WORKOUT_USER_ID = "CycleCoach_activeWorkout_userId";
	private static final String SP_WORKOUT_LAP_NUMBER = "CycleCoach_activeWorkout_lapNumber";
	private static final String SP_WORKOUT_PAUSED = "CycleCoach_activeWorkout_isPaused";
	private static final int SP_DEFAULT_VALUE = -1;
	
	/*****************************************************************
	 *****************************************************************
	 * Database "CycleCoach" --- START
	 *****************************************************************
	 *****************************************************************/
	
	private static final String DB_NAME = "CycleCoach.sqlite";
	private static final int VERSION = 1;
	
	////////////////////////////////////////////////////////////////////
	// Table "sessions"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_SESSIONS = "sessions";
	
	/*****************************************************************
	 * Start Schema for Table "sessions"
	 *****************************************************************/
	
	// Primary key (composite)
	// "_id" auto increments
	private static final String SESSIONS_COL_ID_AUTO_INDEX = "_id";
	private static final String SESSIONS_COL_SESSION_ID = "sessionId";
	
	// Foreign Key - references "user._id"
	private static final String SESSIONS_COL_USER_ID = "userId";
	
	// Member attributes 
	private static final String SESSIONS_COL_LAP_NUMBER = "lapNumber";
	private static final String SESSIONS_COL_START_TIME = "startTimeStamp";
	private static final String SESSIONS_COL_STOP_TIME = "stopTimeStamp";
	private static final String SESSIONS_COL_WHEEL_CIR = "wheelCircumference";
	
	/*****************************************************************
	 * End Schema for Table "sessions"
	 *****************************************************************/
	
	////////////////////////////////////////////////////////////////////
	// Table "heartRate"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_HR = "heartRate";
	
	/*****************************************************************
	 * Start Schema for Table "heart rate"
	 *****************************************************************/
	
	// Primary key
	private static final String HR_COL_TIMESTAMP = "timestamp";
	
	// Foreign key - references "sessions.sessionId"
	private static final String HR_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	// Unit is in beats per minute with a resolution of 1.
	private static final String HR_COL_BPM = "instantBPM";
	
	/*****************************************************************
	 * End Schema for Table "heart rate"
	 *****************************************************************/
	
	////////////////////////////////////////////////////////////////////
	// Table "locations"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_LOC = "locations";
	
	/*****************************************************************
	 * Start Schema for Table "locations"
	 *****************************************************************/
	
	// Primary key
	private static final String LOC_COL_TIMESTAMP = "timestamp";
	
	// Foreign key - references "sessions.sessionId"
	private static final String LOC_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	// Unit is in meters with double precision.
	private static final String LOC_COL_ALTITUDE = "altitude";
	
	// Units are in degrees in the form [+-]DDD.DDDDD
	private static final String LOC_COL_LONGITUDE = "longitude";
	private static final String LOC_COL_LATITUDE = "latitude";
	
	/*****************************************************************
	 * End Schema for Table "locations"
	 *****************************************************************/	
	
	////////////////////////////////////////////////////////////////////
	// Table "cyclePower"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_POW = "cyclePower";
	
	/*****************************************************************
	 * Start Schema for Table "cyclePower"
	 *****************************************************************/
	
	// Primary key
	private static final String POW_COL_TIMESTAMP = "timestamp";
	
	// Foreign key - references "sessions.sessionId"
	private static final String POW_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	// Unit is in watts with a resolution of 1.
	private static final String POW_COL_INSTANT_WATT = "instantPower";
	
	// Unit is in percentage with a resolution of 1/2.
	private static final String POW_COL_PEDAL_POWER_BALANCE = "pedalPowerBalance";
	
	// Unit is in newton meters with a resolution of 1/32.
	private static final String POW_COL_ACCUM_TORQUE = "accumulatedTorque";
	
	// Unitless - requires wheel circumference to calculate speed & distance
	private static final String POW_COL_CUM_WHEEL_REV = "cumulativeWheelRevs";
	
	// Unitless
	private static final String POW_COL_CUM_CRANK_REV = "cumulativeCrankRevs";
	
	// Unit is in newtons with a resolution of 1. 
	private static final String POW_COL_MAX_FORCE = "maximumForceMagnitude";
	
	// Unit is in newtons with a resolution of 1. 
	private static final String POW_COL_MIN_FORCE = "minimumForceMagnitude";
	
	// Unit is in newton metres with a resolution of 1/32. 
	private static final String POW_COL_MAX_TORQUE = "maximumTorqueMagnitude";
	
	// Unit is in newton metres with a resolution of 1/32. 
	private static final String POW_COL_MIN_TORQUE = "minimumTorqueMagnitude";
	
	// Unit is in degrees with a resolution of 1 
	private static final String POW_COL_MAX_ANGLE = "maximumAngle";
	
	// Unit is in degrees with a resolution of 1 
	private static final String POW_COL_MIN_ANGLE = "minimumAngle";
	
	// Unit is in degrees with a resolution of 1 
	private static final String POW_COL_TOP_DEAD_SPOT = "topDeadSpotAngle";
	
	// Unit is in degrees with a resolution of 1 
	private static final String POW_COL_BOTTOM_DEAD_SPOT = "bottomDeadSpotAngle";
	
	// Unit is in kilojoules with a resolution of 1. 
	private static final String POW_COL_ACCUM_ENERGY = "accumulatedEnergy";
	
	
	/*****************************************************************
	 * End Schema for Table "cyclePower"
	 *****************************************************************/	
	
	////////////////////////////////////////////////////////////////////
	// Table "cycleSpeedCadence"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_CSC = "cycleSpeedCadence";
	
	/*****************************************************************
	 * Start Schema for Table "cycleSpeedCadence"
	 *****************************************************************/
	
	// Primary key
	private static final String CSC_COL_TIMESTAMP = "timestamp";
	
	// Foreign key - references "sessions.sessionId"
	private static final String CSC_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	// Unitless - requires wheel circumference to calculate speed & distance
	private static final String CSC_COL_CUM_WHEEL_REV = "cumulativeWheelRevs";
	
	// Unitless
	private static final String CSC_COL_CUM_CRANK_REV = "cumulativeCrankRevs";
	
	/*****************************************************************
	 * End Schema for Table "cycleSpeedCadence"
	 *****************************************************************/
	
	////////////////////////////////////////////////////////////////////
	// Table "workoutSummary"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_WS = "workoutSummary";
	
	/*****************************************************************
	 * Start Schema for Table "workoutSummary"
	 *****************************************************************/
	
	// Primary key
	private static final String WS_COL_ID_AUTO_INDEX = "_id";
	
	// Foreign key - references "sessions.sessionId"
	private static final String WS_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	private static final String WS_COL_TIMESTAMP = "startTimestamp";
	private static final String WS_COL_ELAPSED_TIME = "elapsedTime";
	private static final String WS_COL_WATTS_AVG = "averageWatts";
	private static final String WS_COL_WATTS_MAX = "maxWatts";
	private static final String WS_COL_NPOWER = "nPower";
	private static final String WS_COL_IFACTOR = "iFactor";
	private static final String WS_COL_TSS = "tss";
	private static final String WS_COL_HR_AVG = "averageHeartRate";
	private static final String WS_COL_HR_MAX = "maxHeartRate";
	private static final String WS_COL_CAD_AVG = "averageCadence";
	private static final String WS_COL_CAD_MAX = "maxCadence";
	private static final String WS_COL_SPEED_AVG = "averageSpeed";
	private static final String WS_COL_SPEED_MAX = "maxSpeed";
	private static final String WS_COL_TOTAL_DIST = "totalDistance";
	private static final String WS_COL_CALORIES = "calories";
	
	// Note -- Full workout summary will always have a lapCount
	// of zero. All values greater than zero for lapCount for the
	// same sessionId are subsections of the same workout and should
	// be listed separately under the "Lap" heading.
	private static final String WS_COL_LAP_COUNT = "lapCount";
	
	/*****************************************************************
	 * End Schema for Table "workoutSummary"
	 *****************************************************************/
	
	////////////////////////////////////////////////////////////////////
	// Table "heartPowerZoneStats"
	////////////////////////////////////////////////////////////////////
	private static final String TABLE_HPZ = "heartPowerZoneStats";
	
	/*****************************************************************
	 * Start Schema for Table "heartPowerZoneStats"
	 *****************************************************************/
	
	// Primary key
	private static final String HPZ_COL_ID_AUTO_INDEX = "_id";
	
	// Foreign key - references "sessions.sessionId"
	private static final String HPZ_COL_SESSION_ID = "sessionId";
	
	//
	// Member attributes
	//
	
	private static final String HPZ_COL_HR_ZONE1 = "hrZone1";
	private static final String HPZ_COL_HR_ZONE2 = "hrZone2";
	private static final String HPZ_COL_HR_ZONE3 = "hrZone3";
	private static final String HPZ_COL_HR_ZONE4 = "hrZone4";
	private static final String HPZ_COL_HR_ZONE5 = "hrZone5";
	
	private static final String HPZ_COL_POW_ZONE1 = "powerZone1";
	private static final String HPZ_COL_POW_ZONE2 = "powerZone2";
	private static final String HPZ_COL_POW_ZONE3 = "powerZone3";
	private static final String HPZ_COL_POW_ZONE4 = "powerZone4";
	private static final String HPZ_COL_POW_ZONE5 = "powerZone5";
	
	private static final String HPZ_COL_5SEC_MEAN_MAX_WATTS = "meanMaxWatts_5sec";
	private static final String HPZ_COL_20SEC_MEAN_MAX_WATTS = "meanMaxWatts_20sec";
	private static final String HPZ_COL_1MIN_MEAN_MAX_WATTS = "meanMaxWatts_1min";
	private static final String HPZ_COL_5MIN_MEAN_MAX_WATTS = "meanMaxWatts_5min";
	private static final String HPZ_COL_20MIN_MEAN_MAX_WATTS = "meanMaxWatts_20min";
	
	/*****************************************************************
	 * End Schema for Table "heartPowerZoneStats"
	 *****************************************************************/
	
	/*****************************************************************
	 *****************************************************************
	 * Database "CycleCoach" --- END
	 *****************************************************************
	 *****************************************************************/
	
	private static WorkoutDatabaseHelper sWorkoutDbHelper;
	
	// Constructor
	private WorkoutDatabaseHelper(Context context) {
		super(context, DB_NAME, null, VERSION);
	}
	
	public static WorkoutDatabaseHelper getInstance(Context context) {
		
		if(sWorkoutDbHelper == null) {
			sWorkoutDbHelper = new WorkoutDatabaseHelper(context);
		}
		
		return sWorkoutDbHelper;
	}
	
	
	@Override
	public void onOpen(SQLiteDatabase db) {
		// Set foreign keys enabled
		db.setForeignKeyConstraintsEnabled(true);
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		
		// Create the "sessions" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_SESSIONS + " (" +
				SESSIONS_COL_ID_AUTO_INDEX + " integer primary key autoincrement , " +
				SESSIONS_COL_SESSION_ID + " integer not null, " +
				SESSIONS_COL_USER_ID + " integer, " +
				SESSIONS_COL_LAP_NUMBER + " integer, " +
				SESSIONS_COL_START_TIME + " integer, " +
				SESSIONS_COL_STOP_TIME + " integer, " +
				SESSIONS_COL_WHEEL_CIR + " real)");
		
		// Create the "heartRate" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_HR + " (" +
				HR_COL_TIMESTAMP + " integer primary key not null, " +
				HR_COL_SESSION_ID + " integer not null, " +
				HR_COL_BPM + " integer)");
		
		// Create the "locations" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_LOC + " (" +
				LOC_COL_TIMESTAMP + " integer primary key not null, " +
				LOC_COL_SESSION_ID + " integer  not null, " +
				LOC_COL_ALTITUDE + " real, " +
				LOC_COL_LONGITUDE + " real, " +
				LOC_COL_LATITUDE + " real)");
		
		// Create the "cycleSpeedCadence" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_CSC + " (" +
				CSC_COL_TIMESTAMP + " integer primary key not null, " +
				CSC_COL_SESSION_ID + " integer  not null, " +
				CSC_COL_CUM_WHEEL_REV + " integer, " +
				CSC_COL_CUM_CRANK_REV + " integer)");
		
		// Create the "cyclePower" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_POW + " (" +
				POW_COL_TIMESTAMP + " integer primary key not null, " +
				POW_COL_SESSION_ID + " integer  not null, " +
				POW_COL_INSTANT_WATT + " integer, " +
				POW_COL_PEDAL_POWER_BALANCE + " real, " +
				POW_COL_ACCUM_TORQUE + " real, " +
				POW_COL_CUM_WHEEL_REV + " integer, " +
				POW_COL_CUM_CRANK_REV + " integer, " +
				POW_COL_MAX_FORCE + " integer, " +
				POW_COL_MIN_FORCE + " integer, " +
				POW_COL_MAX_TORQUE + " real, " +
				POW_COL_MIN_TORQUE + " real, " +
				POW_COL_MAX_ANGLE + " integer, " +
				POW_COL_MIN_ANGLE + " integer, " +
				POW_COL_TOP_DEAD_SPOT + " integer, " +
				POW_COL_BOTTOM_DEAD_SPOT + " integer, " +
				POW_COL_ACCUM_ENERGY + " integer)");
		
		// Create the "workoutSummary" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_WS + " (" +
				WS_COL_ID_AUTO_INDEX + " integer primary key autoincrement, " +
				WS_COL_SESSION_ID + " integer  not null, " +
				WS_COL_TIMESTAMP + " integer  not null, " +
				WS_COL_ELAPSED_TIME + " integer, " +
				WS_COL_WATTS_AVG + " integer, " +
				WS_COL_WATTS_MAX + " integer, " +
				WS_COL_NPOWER + " integer, " +
				WS_COL_IFACTOR + " real, " +
				WS_COL_TSS + " real, " +
				WS_COL_HR_AVG + " integer, " +
				WS_COL_HR_MAX + " integer, " +
				WS_COL_CAD_AVG + " integer, " +
				WS_COL_CAD_MAX + " integer, " +
				WS_COL_SPEED_AVG + " real, " +
				WS_COL_SPEED_MAX + " real, " +
				WS_COL_TOTAL_DIST + " real, " +
				WS_COL_CALORIES + " integer, " +
				WS_COL_LAP_COUNT + " integer)");
		
		// Create the "heartPowerZoneStats" table
		db.execSQL("create table IF NOT EXISTS " + TABLE_HPZ + " (" +
				HPZ_COL_ID_AUTO_INDEX + " integer primary key autoincrement, " +
				HPZ_COL_SESSION_ID + " integer, " +
				HPZ_COL_HR_ZONE1 + " integer, " +
				HPZ_COL_HR_ZONE2 + " integer, " +
				HPZ_COL_HR_ZONE3 + " integer, " +
				HPZ_COL_HR_ZONE4 + " integer, " +
				HPZ_COL_HR_ZONE5 + " integer, " +
				HPZ_COL_POW_ZONE1 + " integer, " +
				HPZ_COL_POW_ZONE2 + " integer, " +
				HPZ_COL_POW_ZONE3 + " integer, " +
				HPZ_COL_POW_ZONE4 + " integer, " +
				HPZ_COL_POW_ZONE5 + " integer, " +
				HPZ_COL_5SEC_MEAN_MAX_WATTS + " integer, " +
				HPZ_COL_20SEC_MEAN_MAX_WATTS + " integer, " +
				HPZ_COL_1MIN_MEAN_MAX_WATTS + " integer, " +
				HPZ_COL_5MIN_MEAN_MAX_WATTS + " integer, " +
				HPZ_COL_20MIN_MEAN_MAX_WATTS + " integer)");
		
		db.execSQL("create trigger purge_data_linked_to_session " +
						"before delete on " + TABLE_SESSIONS + " " +
						"for each row begin " +
						"delete from " + TABLE_HR + " where " + 
						TABLE_HR + "." + HR_COL_SESSION_ID + " = " 
						+ SESSIONS_COL_SESSION_ID + ";" +
						"delete from " + TABLE_LOC + " where " + 
						TABLE_LOC + "." + LOC_COL_SESSION_ID + " = "
						+ SESSIONS_COL_SESSION_ID + ";" +
						"delete from " + TABLE_POW + " where " + 
						TABLE_POW + "." + POW_COL_SESSION_ID + " = "
						+ SESSIONS_COL_SESSION_ID + ";" +
						"delete from " + TABLE_CSC + " where " + 
						TABLE_CSC + "." + CSC_COL_SESSION_ID + " = "
						+ SESSIONS_COL_SESSION_ID + ";" +
						"delete from " + TABLE_WS + " where " + 
						TABLE_WS + "." + WS_COL_SESSION_ID + " = "
						+ SESSIONS_COL_SESSION_ID + ";" +
						"delete from " + TABLE_HPZ + " where " + 
						TABLE_HPZ + "." + HPZ_COL_SESSION_ID + " = "
						+ SESSIONS_COL_SESSION_ID + ";" +
						"end;");
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		// TODO Auto-generated method stub

	}
	
	
	/*****************************************************************
	 * User interface functions to start/stop/pause/resume.
	 * 
	 * startNewWorkoutSession()
	 * stopWorkoutSession()
	 * pauseActiveWorkoutSession()
	 * resumePausedWorkoutSession()
	 * startNewLapForActiveSession()
	 * continueStoppedWorkoutSession()
	 * 
	 *****************************************************************/
	
	/* 
	 * startNewWorkoutSession()
	 * 
	 * Inserts a new row in the session table with a unique
	 * sessionId that is a foreign key reference for clients
	 * that insert into heartRate, location, cycleSpeedCadence or
	 * cyclePower tables.
	 * 
	 * Returns: long
	 * 		-1 if failed to update session table
	 * 		Otherwise sessionId >= 0
	 */
	
	public int startNewWorkoutSession(Context context, long userId, double wheelCircumference) {
		int sessionId = -1;
		
		// First find out what the largest sessionId value is
		Cursor c = getReadableDatabase().query(true, // distinct 
				TABLE_SESSIONS, // table
				new String[] { SESSIONS_COL_SESSION_ID }, // columns
				null, // selection
				null, // selectionArgs
				null, // groupBy
				null, // having
				SESSIONS_COL_SESSION_ID + " DESC", // orderBy
				null); // limit
		
		if(c != null && c.getCount() > 0) {
			CursorWrapper cw = new CursorWrapper(c);
			cw.moveToFirst();
			
			if(cw.getColumnCount() != 1) {
				// Error in SQL query, abort
				Log.d(TAG, String.format("startNewWorkoutSession() error, %s", cw.toString()));
				cw.close();
				return sessionId;
			} else {
				sessionId = cw.getInt(0) + 1;
				cw.close();
			}
			
		} else {
			// Nothing exists in the database yet, so set the
			// sessionId to zero.
			sessionId = 0;
			c.close();
		}
		
		long unixTime = Calendar.getInstance().getTimeInMillis();
		ContentValues cv = new ContentValues();
		cv.put(SESSIONS_COL_SESSION_ID, sessionId);
		cv.put(SESSIONS_COL_USER_ID, userId);
		cv.put(SESSIONS_COL_LAP_NUMBER, 1);
		cv.put(SESSIONS_COL_START_TIME, unixTime);
		cv.put(SESSIONS_COL_WHEEL_CIR, wheelCircumference);
		
		long rowIndex = getWritableDatabase().insert(TABLE_SESSIONS, null, cv);
		
		if(rowIndex < 0) {
			// Failed to insert new row into sessions table
			Log.d(TAG, "startNewWorkoutSession() failed to insert into session table");
			return -1;
		}
		
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		SharedPreferences.Editor editor = sp.edit();
		editor.putInt(SP_WORKOUT_SESSION_ID, sessionId);
		editor.putLong(SP_WORKOUT_SESSION_INDEX, rowIndex);
		editor.putLong(SP_WORKOUT_USER_ID, userId);
		editor.putInt(SP_WORKOUT_LAP_NUMBER, 1);
		editor.putBoolean(SP_WORKOUT_PAUSED, false);
		editor.commit();
		
		return sessionId;
	}
	
	/*
	 * stopWorkoutSession()
	 * 
	 * Grabs information about current session from SharedPreferences and
	 * updates the timestamp for that row, which designates it as having 
	 * been stopped. Also clears the SavedPreferences values for this session.
	 *
	 *	Returns: boolean
	 *		TRUE if successfully updated database
	 *		FALSE otherwise
	 */
	public boolean stopActiveWorkoutSession(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		long sessionIndex = sp.getLong(SP_WORKOUT_SESSION_INDEX, SP_DEFAULT_VALUE);
		long userId = sp.getLong(SP_WORKOUT_USER_ID, SP_DEFAULT_VALUE);
		int sessionId = sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE);
		
		if(sessionIndex == SP_DEFAULT_VALUE || userId == SP_DEFAULT_VALUE) {
			// No workout session to stop
			Log.d(TAG, "stopWorkoutSession() called with no active workout to stop.");
			return true;
		}
		
		long unixTime = Calendar.getInstance().getTimeInMillis();
		ContentValues cv = new ContentValues();
		cv.put(SESSIONS_COL_STOP_TIME, unixTime);
		
		int rowsAffected = getWritableDatabase().update(
				TABLE_SESSIONS,
				cv,
				SESSIONS_COL_ID_AUTO_INDEX + " = ?",
				new String[] { String.valueOf(sessionIndex) } );
		
		if(rowsAffected != 1) {
			Log.d(TAG, String.format("stopWorkoutSession() cleared failed to update session table index %d, sessionId %d",
					sessionIndex, sessionId));
			return false;
		} 
		
		SharedPreferences.Editor editor = sp.edit();
		editor.clear();
		editor.commit();
		Log.d(TAG, String.format("stopWorkoutSession() cleared SharedPreferences sessionIndex %d, sessionId %d",
				sessionIndex, sessionId));
		
		return true;
	}
	
	/*
	 * pauseActiveWorkoutSession()
	 * 
	 * Pausing an active session entails setting the stopTime value in the sessionIndex
	 * and marking the SavedPreferences SP_WORKOUT_PAUSED true.
	 * 
	 * Returns: boolean
	 * 		TRUE if session paused and necessary updates were successful.
	 * 		FALSE otherwise.
	 */
	public boolean pauseActiveWorkoutSession(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		long sessionIndex = sp.getLong(SP_WORKOUT_SESSION_INDEX, SP_DEFAULT_VALUE);
		int sessionId = sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE);
		boolean wasPaused = sp.getBoolean(SP_WORKOUT_PAUSED, false);
		
		
		if(sessionIndex == SP_DEFAULT_VALUE) {
			// No workout session to stop
			Log.d(TAG, "pauseActiveWorkoutSession() called with no active workout to pause.");
			return true;
		}
		
		if(wasPaused) {
			Log.d(TAG, String.format("pauseActiveWorkoutSession() called on already paused sessionIndex %d sessionId %d", sessionIndex, sessionId));
			return true;
		}
		
		long unixTime = Calendar.getInstance().getTimeInMillis();
		ContentValues cv = new ContentValues();
		cv.put(SESSIONS_COL_STOP_TIME, unixTime);
		
		int rowsAffected = getWritableDatabase().update(
				TABLE_SESSIONS,
				cv,
				SESSIONS_COL_ID_AUTO_INDEX + " = ?",
				new String[] { String.valueOf(sessionIndex) } );
		
		if(rowsAffected != 1) {
			Log.d(TAG, String.format("pauseActiveWorkoutSession() cleared failed to update session table index %d", sessionIndex));
			return false;
		} 
		
		SharedPreferences.Editor editor = sp.edit();
		editor.putBoolean(SP_WORKOUT_PAUSED, true);
		editor.commit();
		
		Log.d(TAG, String.format("pauseActiveWorkoutSession() on sessionIndex %d sessionId %d", sessionIndex, sessionId));
		return true;
	}
	
	/*
	 * resumePausedWorkoutSession()
	 * 
	 * Restarts a paused workout session. This creates a new row in the session table using the
	 * same sessionId, and the database insert returns a new sessionIndex which is stored
	 * in SavedPreferences.
	 * 
	 * Returns: boolean
	 * 		TRUE if successful
	 * 		FALSE otherwise
	 */
	public boolean resumePausedWorkoutSession(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		long userId = sp.getLong(SP_WORKOUT_USER_ID, SP_DEFAULT_VALUE);
		int sessionId = sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE);
		boolean wasPaused = sp.getBoolean(SP_WORKOUT_PAUSED, false);
		
		if(!wasPaused) {
			Log.d(TAG, String.format("resumePausedWorkoutSession() attempted to restart sessionId %d which was not paused.",
					sessionId));
			return false;
		}
		
		Log.d(TAG, String.format("resumePausedWorkoutSession() sessionId %d, userId %d.", sessionId, userId));
		
		long unixTime = Calendar.getInstance().getTimeInMillis();
		ContentValues cv = new ContentValues();
		cv.put(SESSIONS_COL_SESSION_ID, sessionId);
		cv.put(SESSIONS_COL_USER_ID, userId);
		cv.put(SESSIONS_COL_LAP_NUMBER, 1);
		cv.put(SESSIONS_COL_START_TIME, unixTime);
		
		long rowIndex = getWritableDatabase().insert(TABLE_SESSIONS, null, cv);
		
		if(rowIndex < 0) {
			// Failed to insert new row into sessions table
			Log.d(TAG, "resumePausedWorkoutSession() failed to insert into session table");
			return false;
		}
		
		SharedPreferences.Editor editor = sp.edit();
		editor.putLong(SP_WORKOUT_SESSION_ID, sessionId);
		editor.putLong(SP_WORKOUT_SESSION_INDEX, rowIndex);
		editor.putLong(SP_WORKOUT_USER_ID, userId);
		editor.putInt(SP_WORKOUT_LAP_NUMBER, 1);
		editor.putBoolean(SP_WORKOUT_PAUSED, false);
		editor.commit();
		
		
		return true;
	}
	
	/*
	 * startNewLapForActiveSession()
	 * 
	 * Close out the current sessionIndex and create a new one with the same
	 * sessionId and incremented lap count.
	 * 
	 * Returns: boolean
	 * 		TRUE if successful
	 * 		FALSE otherwise
	 */
	public boolean startNewLapForActiveSession(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		long sessionIndex = sp.getLong(SP_WORKOUT_SESSION_INDEX, SP_DEFAULT_VALUE);
		long userId = sp.getLong(SP_WORKOUT_USER_ID, SP_DEFAULT_VALUE);
		int sessionId = sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE);
		int lapNumber = sp.getInt(SP_WORKOUT_LAP_NUMBER, SP_DEFAULT_VALUE);
		boolean wasPaused = sp.getBoolean(SP_WORKOUT_PAUSED, true);
		
		if(wasPaused) {
			Log.d(TAG, String.format("startNewLapForActiveSession() tried to increment lap for paused session %d", sessionId));
			return false;
		}
		
		
		// Close the old sessionIndex
		long unixTime = Calendar.getInstance().getTimeInMillis();
		ContentValues cv = new ContentValues();
		cv.put(SESSIONS_COL_STOP_TIME, unixTime);
		
		int rowsAffected = getWritableDatabase().update(
				TABLE_SESSIONS,
				cv,
				SESSIONS_COL_ID_AUTO_INDEX + " = ?",
				new String[] { String.valueOf(sessionIndex) } );
		
		if(rowsAffected != 1) {
			Log.d(TAG, String.format("startNewLapForActiveSession() cleared failed to update session table index %d", sessionIndex));
			return false;
		}
		
		// Increment the lap number
		lapNumber++;
		
		cv = new ContentValues();
		cv.put(SESSIONS_COL_SESSION_ID, sessionId);
		cv.put(SESSIONS_COL_USER_ID, userId);
		cv.put(SESSIONS_COL_LAP_NUMBER, lapNumber);
		cv.put(SESSIONS_COL_START_TIME, unixTime);
		
		sessionIndex = getWritableDatabase().insert(TABLE_SESSIONS, null, cv);
		
		if(sessionIndex < 0) {
			// Failed to insert new row into sessions table
			Log.d(TAG, "startNewLapForActiveSession() failed to insert into session table");
			return false;
		}
		
		SharedPreferences.Editor editor = sp.edit();
		editor.putLong(SP_WORKOUT_SESSION_INDEX, sessionIndex);
		editor.putInt(SP_WORKOUT_LAP_NUMBER, lapNumber);
		editor.commit();
		
		
		return true;
	}
	

	/*
	 * continueStoppedWorkoutSession()
	 * 
	 * Continue where the user left off in the workout for the
	 * given sessionId.
	 */
	public boolean continueStoppedWorkoutSession(Context context, int sessionId) {
		// TODO
		return false;
	}
	
	/*****************************************************************
	 * Service functions for adding data to the various tables.
	 * 
	 * isWorkoutSessionActive()
	 * getActiveSessionId()
	 * insertTable_heartRate()
	 * insertTable_locations()
	 * insertTable_cyclePower()
	 * insertTable_cycleSpeedCadence()
	 * createWorkoutSessionSummary()
	 * 
	 *****************************************************************/
	
	/*
	 * isWorkoutSessionActive()
	 * 
	 * Checks to see if there is an active workout session.
	 * 
	 * Returns: long
	 * 		If value > -1 then userId that owns current session was returned
	 * 		Otherwise no session is active
	 * 
	 */
	public long isWorkoutSessionActive(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		long userId = sp.getLong(SP_WORKOUT_USER_ID, SP_DEFAULT_VALUE);
		
		if(userId != SP_DEFAULT_VALUE) {
			return userId;
		}
		else {
			return SP_DEFAULT_VALUE;
		}
	}
	
	/*
	 * getActiveSessionId()
	 * 
	 * Gets the sessionId of the current active session.
	 * 
	 * Returns: long
	 * 		If value > -1 then sessionId of active session was returned
	 * 		Otherwise no session is active
	 * 
	 */
	public int getActiveSessionId(Context context) {
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		return sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE);
	}
	
	
	/*
	 * insertTable_heartRate()
	 * 
	 * Inserts timestamp and BPM for sessionId to heartRate table.
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_heartRate(long timestamp, int sessionId, int bpm) {
		
		ContentValues cv = new ContentValues();
		cv.put(HR_COL_TIMESTAMP, timestamp);
		cv.put(HR_COL_SESSION_ID, sessionId);
		cv.put(HR_COL_BPM, bpm);
		
		long index = getWritableDatabase().insert(TABLE_HR, null, cv);
		
		// Check if insert was successful
		if(index >= 0) return true;
		
		return false;
	}
	
	/*
	 * insertTable_locations()
	 * 
	 * Inserts the following into the locations table for sessionId:
	 * 		long timestamp
	 * 		double altitude
	 * 		double longitude
	 * 		double latitude
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_locations(
			long timestamp, int sessionId, double altitude, double longitude, double latitude) {
		ContentValues cv = new ContentValues();
		cv.put(LOC_COL_TIMESTAMP, timestamp);
		cv.put(LOC_COL_SESSION_ID, sessionId);
		cv.put(LOC_COL_ALTITUDE, altitude);
		cv.put(LOC_COL_LONGITUDE, longitude);
		cv.put(LOC_COL_LATITUDE, latitude);
		
		long index = getWritableDatabase().insert(TABLE_LOC, null, cv);
		
		// Check if insert was successful
		if(index >= 0) return true;
		
		return false;
	}
	
	/*
	 * insertTable_cyclePower()
	 * 
	 * Inserts the following into the locations table for sessionId:
	 * 		long timestamp
	 * 		int instantPower
	 * 		double pedalPowerBalance
	 * 		double accumulatedTorque
	 * 		int cumWheelRevs
	 * 		int cumCrankRevs
	 * 		int maxForce
	 * 		int minForce
	 * 		double maxTorque
	 * 		double minTorque
	 * 		int maxAngle
	 * 		int minAngle
	 * 		int topDeadSpotAngle
	 * 		int bottomDeadSpotAngle
	 * 		int accumulatedEnergy
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_cyclePower(
			long timestamp, int sessionId, int instantPower, double pedalPowerBalance, double accumulatedTorque,
			int cumWheelRevs, int cumCrankRevs, int maxForce, int minForce, double maxTorque, double minTorque,
			int maxAngle, int minAngle, int topDeadSpotAngle, int bottomDeadSpotAngle, int accumulatedEnergy) {
		
		ContentValues cv = new ContentValues();
		cv.put(POW_COL_TIMESTAMP, timestamp);
		cv.put(POW_COL_SESSION_ID, sessionId);
		cv.put(POW_COL_INSTANT_WATT, instantPower);
		cv.put(POW_COL_PEDAL_POWER_BALANCE, pedalPowerBalance);
		cv.put(POW_COL_ACCUM_TORQUE, accumulatedTorque);
		cv.put(POW_COL_CUM_WHEEL_REV, cumWheelRevs);
		cv.put(POW_COL_CUM_CRANK_REV, cumCrankRevs);
		cv.put(POW_COL_MAX_FORCE, maxForce);
		cv.put(POW_COL_MIN_FORCE, minForce);
		cv.put(POW_COL_MAX_TORQUE, maxTorque);
		cv.put(POW_COL_MIN_TORQUE, minTorque);
		cv.put(POW_COL_MAX_ANGLE, maxAngle);
		cv.put(POW_COL_MIN_ANGLE, minAngle);
		cv.put(POW_COL_TOP_DEAD_SPOT, topDeadSpotAngle);
		cv.put(POW_COL_BOTTOM_DEAD_SPOT, bottomDeadSpotAngle);
		cv.put(POW_COL_ACCUM_ENERGY, accumulatedEnergy);
		
		long index = getWritableDatabase().insert(TABLE_POW, null, cv);
		
		// Check if insert was successful
		if(index >= 0) return true;
		
		return false;
	}
	
	/*
	 * insertTable_cyclePower()
	 * 
	 * Inserts the following into the cyclePower table for sessionId:
	 * 		long timestamp
	 * 		int instantPower
	 * 		double accumulatedTorque
	 * 		int cumWheelRevs
	 * 		int cumCrankRevs
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_cyclePower(
			long timestamp, int sessionId, int instantPower,
			double accumulatedTorque, int cumWheelRevs, int cumCrankRevs) {
		return insertTable_cyclePower(
				timestamp, sessionId, instantPower, 0.0, accumulatedTorque,
				cumWheelRevs, cumCrankRevs, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0);
	}
	
	/*
	 * insertTable_cyclePower()
	 * 
	 * Inserts the following into the cyclePower table for sessionId:
	 * 		long timestamp
	 * 		int instantPower
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_cyclePower(
			long timestamp, int sessionId, int instantPower) {
		return insertTable_cyclePower(
				timestamp, sessionId, instantPower, 0.0, 0,
				0, -1, 0, 0, 0.0, 0.0, 0, 0, 0, 0, 0);
	}
	
	/*
	 * insertTable_cycleSpeedCadence()
	 * 
	 * Inserts the following into the cycleSpeedCadence table for sessionId:
	 * 		long timestamp
	 * 		int cumWheelRevs
	 * 		int cumCrankRevs
	 * 
	 * Returns: boolean
	 * 		TRUE if successfully inserted
	 * 		FALSE otherwise
	 * 
	 */
	public boolean insertTable_cycleSpeedCadence (long timestamp, int sessionId, int cumWheelRevs, int cumCrankRevs) {
		
		ContentValues cv = new ContentValues();
		cv.put(CSC_COL_TIMESTAMP, timestamp);
		cv.put(CSC_COL_SESSION_ID, sessionId);
		cv.put(CSC_COL_CUM_WHEEL_REV, cumWheelRevs);
		cv.put(CSC_COL_CUM_CRANK_REV, cumCrankRevs);
		
		long index = getWritableDatabase().insert(TABLE_CSC, null, cv);
		
		// Check if insert was successful
		if(index >= 0) return true;
		
		return false;
	}
	
	/*
	 * createWorkoutSessionSummary()
	 * 
	 * This function queries data from all the tables for the sessionId
	 * and creates a summary of that data that is saved in the
	 * workoutSummary table. This table is where the user workout
	 * history data should be pulled from.
	 * 
	 * Returns: boolean
	 * 		TRUE if successful
	 * 		FALSE otherwise
	 * 
	 */
	public boolean createWorkoutSessionSummary(long sessionId) {
		
		// First check if a summary has been created for this session yet.
		Cursor c = getReadableDatabase().query(
				TABLE_WS, // table
				new String[] { SESSIONS_COL_SESSION_ID }, // columns
				WS_COL_SESSION_ID + " = ?", // selection
				new String[] { String.valueOf(sessionId) }, // selectionArgs
				null, // groupBy
				null, // having
				null, // orderBy
				null); // limit
		
		if(c != null && c.getCount() > 0)
			// Workout summary already exists, just return
			return true;
		
		int lapCount = 1;
		
		// Grab the session table entries
		c = getReadableDatabase().query(
			TABLE_SESSIONS, // table
			null, // columns
			SESSIONS_COL_SESSION_ID + " = ?", // selection
			new String[] { String.valueOf(sessionId) }, // selectionArgs
			null, // groupBy
			null, // having
			SESSIONS_COL_SESSION_ID, // orderBy
			null); // limit
		
		if(c == null || c.getCount() == 0) {
			Log.d(TAG, String.format("No data found in sessions table for sessionId %d", sessionId));
			return false;
		}
		
		// elapsedTime will contain a list of all the start/stop time differences found
		// in all of the subsessions logged. In cases where the user just paused but did
		// not increment the lap counter, the subsession times will be merged into one.
		//
		// The last value entered will be the sum of all subsession times in the list.
		ArrayList<Long> startTimeList = new ArrayList<Long>();
		ArrayList<Long> stopTimeList = new ArrayList<Long>();
		ArrayList<Integer> lapList = new ArrayList<Integer>();
		ArrayList<Long> elapsedTime = new ArrayList<Long>();
		
		CursorWrapper cw = new CursorWrapper(c);
		cw.moveToFirst();
		double wheelCircumference = cw.getDouble(cw.getColumnIndex(SESSIONS_COL_WHEEL_CIR));
		long workoutStartTime = cw.getLong(cw.getColumnIndex(SESSIONS_COL_START_TIME));
		while(!cw.isAfterLast()) {
			long startTime = cw.getLong(cw.getColumnIndex(SESSIONS_COL_START_TIME));
			long stopTime = cw.getLong(cw.getColumnIndex(SESSIONS_COL_STOP_TIME));
			startTimeList.add(startTime);
			stopTimeList.add(stopTime);
			elapsedTime.add(stopTime - startTime);
			int currLap = cw.getInt(cw.getColumnIndex(SESSIONS_COL_LAP_NUMBER));
			lapList.add(currLap);
			
			if(currLap > lapCount) lapCount = currLap; // increment the lap counter
			
			// Get the next row from our query
			cw.moveToNext();
		}
		
		// Close out the sessions table query
		cw.close();
		
		
		// Try and grab speedCadence data (if it was logged).
		ArrayList<Long> crankRevs = new ArrayList<Long>();
		ArrayList<Double> distance = new ArrayList<Double>();
		double maxCrankRPM = 0; // revolutions per minute
		double maxSpeed = 0; // meters per second
		boolean completeSpeedCadenceDataAcquired = true;
		
		for(int i = 0; i < startTimeList.size(); i++) {
			long startTime = startTimeList.get(i);
			long stopTime = stopTimeList.get(i);
			
			// Grab the full speedCadence table between the specified timestamps
			c = getReadableDatabase().query(
					TABLE_CSC, // table
					new String[] { CSC_COL_TIMESTAMP, 
							CSC_COL_CUM_WHEEL_REV,
							CSC_COL_CUM_CRANK_REV }, // columns
					CSC_COL_TIMESTAMP + " > ? AND " + CSC_COL_TIMESTAMP + " <= ?", // selection
					new String[] { String.valueOf(startTime), String.valueOf(stopTime) }, // selectionArgs
					null, // groupBy
					null, // having
					CSC_COL_TIMESTAMP, // orderBy
					null); // limit
			
			if(c == null || c.getCount() == 0) {
				Log.d(TAG, String.format("No data found in cycleSpeedCadence table between %d and %d", startTime, stopTime));
				crankRevs.add((long) -1);
				distance.add(0.0);
				completeSpeedCadenceDataAcquired = false;
				continue;
			}
			
			cw = new CursorWrapper(c);
			cw.moveToFirst();
			
			long startCrankRevs = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_CRANK_REV));
			long startWheelRevs = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_WHEEL_REV));
			
			cw.moveToLast();
			
			long endCrankRevs = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_CRANK_REV));
			long endWheelRevs = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_WHEEL_REV));
			
			crankRevs.add(endCrankRevs - startCrankRevs);
			distance.add((endWheelRevs - startWheelRevs) * wheelCircumference);
			
			
			// Calculate meters traveled and crank revs between each time period
			// so we can figure out maxCrankRPM and maxSpeed
			cw.moveToFirst();
			
			long lastCrankRev = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_CRANK_REV));
			long lastWheelRev = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_WHEEL_REV));
			long lastTimestamp = cw.getLong(cw.getColumnIndex(CSC_COL_TIMESTAMP));
			cw.moveToNext();
			
			while(!cw.isAfterLast()) {
				
				long currCrankRev = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_CRANK_REV));
				long currWheelRev = cw.getLong(cw.getColumnIndex(CSC_COL_CUM_WHEEL_REV));
				long currTimestamp = cw.getLong(cw.getColumnIndex(CSC_COL_TIMESTAMP));
				
				double timeElapsedSec = (currTimestamp - lastTimestamp) / 1000;
				
				long wheelRevCount = currWheelRev = lastWheelRev;
				double distanceTraveled = wheelRevCount * wheelCircumference;
				double speed = distanceTraveled / timeElapsedSec;
				
				if(speed > maxSpeed) maxSpeed = speed;
				
				long crankRevCount = currCrankRev - lastCrankRev;
				double crankRPM = ((double)crankRevCount / (double)timeElapsedSec) * 60;
				
				if(crankRPM > maxCrankRPM) maxCrankRPM = crankRPM;
				
				lastCrankRev = currCrankRev;
				lastWheelRev = currWheelRev;
				cw.moveToNext();
			}
			
			// Close out the speedCadence table query
			cw.close();
		}
		
		
		// Retrieve cycle power data (if it was logged).
		ArrayList<Long> totalWatts = new ArrayList<Long>();
		ArrayList<Long> powerTableQueryRows = new ArrayList<Long>();
		int maxPower = 0;
		boolean completePowerDataAcquired = false;
		
		for(int i = 0; i < startTimeList.size(); i++) {
			long startTime = startTimeList.get(i);
			long stopTime = stopTimeList.get(i);
			
			// Grab the sum of the instant power entries logged and the row count.
			c = getReadableDatabase().query(
					TABLE_POW, // table
					new String[] { "sum(" + POW_COL_INSTANT_WATT + ")",
							"count(" + POW_COL_INSTANT_WATT + ")",
							"max(" + POW_COL_INSTANT_WATT + ")" }, // columns
					POW_COL_TIMESTAMP + " > ? AND " + POW_COL_TIMESTAMP + " <= ?", // selection
					new String[] { String.valueOf(startTime), String.valueOf(stopTime) }, // selectionArgs
					null, // groupBy
					null, // having
					null, // orderBy
					null); // limit
			
			if(c == null || c.getCount() == 0) {
				Log.d(TAG, String.format("No data found in cyclePower table between %d and %d", startTime, stopTime));
				totalWatts.add((long) -1);
				powerTableQueryRows.add((long) 0);
				completePowerDataAcquired = false;
				continue;
			}
			
			cw = new CursorWrapper(c);
			cw.moveToFirst();
			totalWatts.add(cw.getLong(0));
			powerTableQueryRows.add(cw.getLong(1));
			int maxVal = cw.getInt(2);
			if(maxVal > maxPower) {
				maxPower = maxVal;
			}
			
			// Close out the cyclePower table query.
			cw.close();
			
			// Check if we still need speedCadence data, and try to grab it
			// from this table if available.
			if(crankRevs.get(i) == -1) {
				// Try and grab speed and cadence data from
				// power table between the specified timestamps
				c = getReadableDatabase().query(
						TABLE_POW, // table
						new String[] { POW_COL_TIMESTAMP, 
								POW_COL_CUM_WHEEL_REV,
								POW_COL_CUM_CRANK_REV }, // columns
						POW_COL_TIMESTAMP + " > ? AND " + POW_COL_TIMESTAMP + " <= ?", // selection
						new String[] { String.valueOf(startTime), String.valueOf(stopTime) }, // selectionArgs
						null, // groupBy
						null, // having
						POW_COL_TIMESTAMP, // orderBy
						null); // limit
				
				if(c == null || c.getCount() == 0) {
					Log.d(TAG, String.format("No data found in cyclePower table between %d and %d", startTime, stopTime));
					continue;
				}
				
				cw = new CursorWrapper(c);
				cw.moveToFirst();
				
				// Verify that there is valid data in the table for this time period
				if(cw.getLong(cw.getColumnIndex(POW_COL_CUM_CRANK_REV)) == -1) {
					// Nothing was logged in the cyclePower table either for
					// speed and cadence. Stop here.
					cw.close();
					Log.d(TAG, String.format("No data found in cyclePower table between %d and %d for speed/cadence", startTime, stopTime));
					continue;
				}
				
				long startCrankRevs = cw.getLong(cw.getColumnIndex(POW_COL_CUM_CRANK_REV));
				long startWheelRevs = cw.getLong(cw.getColumnIndex(POW_COL_CUM_WHEEL_REV));
				
				cw.moveToLast();
				
				long endCrankRevs = cw.getLong(cw.getColumnIndex(POW_COL_CUM_CRANK_REV));
				long endWheelRevs = cw.getLong(cw.getColumnIndex(POW_COL_CUM_WHEEL_REV));
				
				crankRevs.add(endCrankRevs - startCrankRevs);
				distance.add((endWheelRevs - startWheelRevs) * wheelCircumference);
				
				// Calculate meters traveled and crank revs between each time period
				// so we can figure out maxCrankRPM and maxSpeed
				cw.moveToFirst();
				
				long lastCrankRev = cw.getLong(cw.getColumnIndex(POW_COL_CUM_CRANK_REV));
				long lastWheelRev = cw.getLong(cw.getColumnIndex(POW_COL_CUM_WHEEL_REV));
				long lastTimestamp = cw.getLong(cw.getColumnIndex(POW_COL_TIMESTAMP));
				cw.moveToNext();
				
				while(!cw.isAfterLast()) {
					
					long currCrankRev = cw.getLong(cw.getColumnIndex(POW_COL_CUM_CRANK_REV));
					long currWheelRev = cw.getLong(cw.getColumnIndex(POW_COL_CUM_WHEEL_REV));
					long currTimestamp = cw.getLong(cw.getColumnIndex(POW_COL_TIMESTAMP));
					
					double timeElapsedSec = (currTimestamp - lastTimestamp) / 1000;
					
					long wheelRevCount = currWheelRev = lastWheelRev;
					double distanceTraveled = wheelRevCount * wheelCircumference;
					double speed = distanceTraveled / timeElapsedSec; // meters per second
					
					if(speed > maxSpeed) maxSpeed = speed;
					
					long crankRevCount = currCrankRev - lastCrankRev;
					double crankRPM = (crankRevCount / timeElapsedSec) * 60;
					
					if(crankRPM > maxCrankRPM) maxCrankRPM = crankRPM;
					
					lastCrankRev = currCrankRev;
					lastWheelRev = currWheelRev;
					cw.moveToNext();
				}
				
				// Close out the cyclePower table query for speed and cadence data
				cw.close();
			}
		}
		
		// If speedCadenceDataAcquired or powerDataAcquired is still false at this
		// point try using data from the location table to estimate these.
		// 
		if(!completeSpeedCadenceDataAcquired || !completePowerDataAcquired) {
			for(int i = 0; i < startTimeList.size(); i++) {
				long startTime = startTimeList.get(i);
				long stopTime = stopTimeList.get(i);
				
				// Grab the sum of the instant power entries logged and the row count.
				c = getReadableDatabase().query(
						TABLE_LOC, // table
						new String[] { LOC_COL_TIMESTAMP, 
								LOC_COL_ALTITUDE,
								LOC_COL_LONGITUDE,
								LOC_COL_LATITUDE}, // columns
								LOC_COL_TIMESTAMP + " > ? AND " + LOC_COL_TIMESTAMP + " <= ?", // selection
						new String[] { String.valueOf(startTime), String.valueOf(stopTime) }, // selectionArgs
						null, // groupBy
						null, // having
						LOC_COL_TIMESTAMP, // orderBy
						null); // limit
				
				if(c == null || c.getCount() == 0) {
					Log.d(TAG, String.format("No data found in location table between %d and %d", startTime, stopTime));
					continue;
				}
				
				// Use the longitude/latitude to get speed/distance data
				// TODO
				
				// Use the longitude/latitude/altitude to estimate watts generated
				// TODO
			}
		}
		
		
		// Retrieve heart rate data (if it was logged).
		ArrayList<Long> totalBPM = new ArrayList<Long>();
		ArrayList<Long> heartRateTableQueryRows = new ArrayList<Long>();
		int maxHR = 0;
		boolean heartRateDataAcquired = false;
		for(int i = 0; i < startTimeList.size(); i++) {
			long startTime = startTimeList.get(i);
			long stopTime = stopTimeList.get(i);
			
			// Grab the sum of the heart rate BPM logged and the row count.
			c = getReadableDatabase().query(
					TABLE_HR, // table
					new String[] { "sum(" + HR_COL_BPM + ")",
							"count(" + HR_COL_BPM + ")",
							"max(" + HR_COL_BPM + ")" }, // columns
					HR_COL_TIMESTAMP + " > ? AND " + HR_COL_TIMESTAMP + " <= ?", // selection
					new String[] { String.valueOf(startTime), String.valueOf(stopTime) }, // selectionArgs
					null, // groupBy
					null, // having
					null, // orderBy
					null); // limit
			
			if(c == null || c.getCount() == 0) {
				Log.d(TAG, String.format("No data found in heartRate table between %d and %d", startTime, stopTime));
				totalBPM.add((long) 0);
				heartRateTableQueryRows.add((long) 0);
				continue;
			}
			
			cw = new CursorWrapper(c);
			cw.moveToFirst();
			totalBPM.add(cw.getLong(0));
			heartRateTableQueryRows.add(cw.getLong(1));
			int maxVal = cw.getInt(2);
			if(maxVal > maxHR) {
				maxHR = maxVal;
			}
			
			// Close out the heartRate table query
			cw.close();
			
			heartRateDataAcquired = true;
		}
		
		
		// NOTE:
		//   totalWatts() at index is set to -1 if no power data acquired for that time interval
		//   crankRevs() at index is set to -1 if no speed/cadence data acquired for that interval
		
		
		
		ArrayList<Integer> calories = new ArrayList<Integer>();
		// Calculate calories burned for each interval
		// Formula: AvgWatts * hours * 3.6 = calories
		if(totalWatts.size() > 0) {
			for(int i = 0; i < totalWatts.size(); i++) {
				double elapsedTimeInHours = elapsedTime.get(i) / MILLISEC_TO_HOURS;
				double avgWatts = ((double) totalWatts.get(i)) / ((double) powerTableQueryRows.get(i));
				calories.add((int) Math.floor(elapsedTimeInHours * avgWatts * 3.6));
			}
		} else if (crankRevs.size() > 0){
			// Try using the distance traveled and user weight to estimate
			// calories burned.
			
		}
		
		// TODO - handling just the simple case for now... no pauses or lap increments
		ContentValues cv = new ContentValues();
		cv.put(WS_COL_TIMESTAMP, workoutStartTime);
		cv.put(WS_COL_ELAPSED_TIME, elapsedTime.get(0));
		cv.put(WS_COL_WATTS_AVG, (int) (Math.floor(totalWatts.get(0) / powerTableQueryRows.get(0))));
		cv.put(WS_COL_WATTS_MAX, maxPower);
		cv.put(WS_COL_NPOWER, 0);
		cv.put(WS_COL_IFACTOR, 0);
		cv.put(WS_COL_TSS, 0);
		cv.put(WS_COL_HR_AVG, (int) (Math.floor(totalBPM.get(0) / heartRateTableQueryRows.get(0))));
		cv.put(WS_COL_HR_MAX, maxHR);
		cv.put(WS_COL_CAD_AVG, (int)(Math.floor(((double)crankRevs.get(0)) / (((double)elapsedTime.get(0)) / 1000 / 60))));
		cv.put(WS_COL_CAD_MAX, maxCrankRPM);
		cv.put(WS_COL_SPEED_AVG, distance.get(0) / ((double) elapsedTime.get(0)) * 3600.0); // kph
		cv.put(WS_COL_SPEED_MAX, maxSpeed * 3600.0 / 100000.0); // kph
		cv.put(WS_COL_TOTAL_DIST, distance.get(0) / 1000);
		cv.put(WS_COL_CALORIES, calories.get(0));
		cv.put(WS_COL_LAP_COUNT, 0);
		cv.put(WS_COL_SESSION_ID, sessionId);
		
		
		
		long workoutSummaryIndex = getWritableDatabase().insert(TABLE_WS, null, cv);
		
		if(workoutSummaryIndex < 0) return false;
		
		
		
		//UserProfile userProfile
		//long workoutStartTime;
		//int lapCount;
		//int maxPower;
		//double wheelCircumference;
		//ArrayList<Long> startTimeList;
		//ArrayList<Long> stopTimeList;
		//ArrayList<Integer> lapList;
		//ArrayList<Long> elapsedTime;
		//ArrayList<Long> totalWatts;
		//ArrayList<Long> powerTableQueryRows;
		//ArrayList<Long> totalBPM;
		//ArrayList<Long> heartRateTableQueryRows;
		//int maxHR;
		//ArrayList<Long> crankRevs;
		//ArrayList<Double> distance;
		//double maxCrankRPM; // revolutions per minute
		//double maxSpeed; // meters per second
		
		// Summary values
		// TODO
		//int nPower = 0;
		//double iFactor = 0;
		//double tss = 0;
		
		
		
		return true;
	}
	
	/*****************************************************************
	 * User functions for exporting, deleting and importing data.
	 * 
	 * purgeAllWorkoutDataForUser()
	 * deleteWorkoutSession()
	 * importWorkoutData()
	 * 
	 *****************************************************************/
	
	public boolean deleteWorkoutSession(int sessionId) {
		
		int rowsDeleted = 
				getReadableDatabase().delete(
					TABLE_SESSIONS,
					SESSIONS_COL_SESSION_ID + " = ?",
					new String[] { String.valueOf(sessionId) });
		return (rowsDeleted > 0);
	}
	

	
	/*****************************************************************
	 * Test/debug functions
	 * 
	 * getTableSchema()
	 * getTableData()
	 * insertTableData()
	 * 
	 *****************************************************************/
	
	
	public void testScript(final Context context) {
		
		Thread thread = new Thread() {
			@Override
			public void run() {
				try {
					synchronized(this) {
						Log.d(TAG, "Starting test script");
						Log.d(TAG, "Calling startNewWorkoutSession");
						
						writeQueryToDebugLog(
								true,
								TABLE_SESSIONS,
								new String[] { "(SELECT MAX(" + SESSIONS_COL_SESSION_ID + "))" },
								null,
								null);
						
						int sessionId = startNewWorkoutSession(context, 31337, 2.096);
						writeQueryToDebugLog(
								false,
								TABLE_SESSIONS,
								null,
								SESSIONS_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId)});
						writeSharedPrefToDebugLog(context);
						
						long timestamp = Calendar.getInstance().getTimeInMillis();
						
						insertTable_heartRate(timestamp, sessionId, 125);
						insertTable_locations(timestamp, sessionId, 10, 80.0, -71.0);
						insertTable_cyclePower(timestamp, sessionId, 205);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 500, 200);
						wait(1000);
						
						timestamp = Calendar.getInstance().getTimeInMillis();
						insertTable_heartRate(timestamp, sessionId, 130);
						insertTable_locations(timestamp, sessionId, 10, 80.1, -71.1);
						insertTable_cyclePower(timestamp, sessionId, 195);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 504, 202);
						wait(1000);
						
						timestamp = Calendar.getInstance().getTimeInMillis();
						insertTable_heartRate(timestamp, sessionId, 135);
						insertTable_locations(timestamp, sessionId, 10, 80.2, -71.2);
						insertTable_cyclePower(timestamp, sessionId, 205);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 509, 204);
						wait(1000);
						
						timestamp = Calendar.getInstance().getTimeInMillis();
						insertTable_heartRate(timestamp, sessionId, 140);
						insertTable_locations(timestamp, sessionId, 10, 80.3, -71.3);
						insertTable_cyclePower(timestamp, sessionId, 210);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 513, 206);
						wait(1000);
						
						timestamp = Calendar.getInstance().getTimeInMillis();
						insertTable_heartRate(timestamp, sessionId, 138);
						insertTable_locations(timestamp, sessionId, 10, 80.4, -71.4);
						insertTable_cyclePower(timestamp, sessionId, 190);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 518, 208);
						wait(1000);
						
						timestamp = Calendar.getInstance().getTimeInMillis();
						insertTable_heartRate(timestamp, sessionId, 135);
						insertTable_locations(timestamp, sessionId, 10, 80.5, -71.5);
						insertTable_cyclePower(timestamp, sessionId, 215);
						insertTable_cycleSpeedCadence(timestamp, sessionId, 522, 210);
						wait(1000);
						
						
						writeQueryToDebugLog(
								false,
								TABLE_HR,
								null,
								HR_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_LOC,
								null,
								LOC_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_POW,
								null,
								POW_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_CSC,
								null,
								CSC_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						// Check that the table schema are correct for the "sessions" table.
						stopActiveWorkoutSession(context);
						writeSharedPrefToDebugLog(context);
						
						writeQueryToDebugLog(
								false,
								TABLE_SESSIONS,
								null,
								SESSIONS_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId)});
						
						createWorkoutSessionSummary(sessionId);
						
						writeQueryToDebugLog(
								false,
								TABLE_WS,
								null,
								WS_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						
						deleteWorkoutSession(sessionId);
						writeQueryToDebugLog(
								false,
								TABLE_SESSIONS,
								null,
								SESSIONS_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_HR,
								null,
								HR_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_LOC,
								null,
								LOC_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_POW,
								null,
								POW_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
						
						writeQueryToDebugLog(
								false,
								TABLE_CSC,
								null,
								CSC_COL_SESSION_ID + " = ?",
								new String[] { String.valueOf(sessionId) });
					}
				} catch(InterruptedException e) {
					
				}
			}
		};
		
		thread.start();
		
		
		
		
	}
	
	public void writeQueryToDebugLog(boolean distinct, String table, String[] columns, String selection, String[] args) {
		StringBuilder s = new StringBuilder();
		Cursor c = getReadableDatabase().query(
				distinct,
				table, // table
				columns, // columns
				selection, // selection
				args, // selectionArgs
				null, // groupBy
				null, // having
				null, // orderBy
				null); // limit
		
		if(c != null && c.getCount() > 0) {
			CursorWrapper cw = new CursorWrapper(c);
			
			s.append("columns: ");
			int colCount = cw.getColumnCount();
			for(int i = 0; i < colCount; i++) {
				s.append(cw.getColumnName(i) + " ");
			}
			
			s.append("\n");
			
			cw.moveToFirst();
			
			while(!cw.isAfterLast()) {
				for(int i = 0; i < colCount; i++) {
					switch(cw.getType(i)) {
					case Cursor.FIELD_TYPE_NULL:
						s.append("null");
						break;
					case Cursor.FIELD_TYPE_FLOAT:
						s.append(Float.toString(cw.getFloat(i)));
						break;
					case Cursor.FIELD_TYPE_INTEGER:
						s.append(Integer.toString(cw.getInt(i)));
						break;
					case Cursor.FIELD_TYPE_STRING:
						s.append(cw.getString(i));
						break;
					case Cursor.FIELD_TYPE_BLOB:
						// ignore
						break;
					}
					
					s.append(" ");
				}
				
				s.append("\n");
				cw.moveToNext();
			}
		}
		else {
			s.append(table + " " + selection);
			s.append(" query returned empty.");
		}
		
		Log.d(TAG, s.toString());
	}
	
	public void writeSharedPrefToDebugLog(Context context) {
		StringBuilder s = new StringBuilder();
		SharedPreferences sp = context.getSharedPreferences(SP_WORKOUT_SESSION, Context.MODE_PRIVATE);
		s.append("sessionId = " + sp.getInt(SP_WORKOUT_SESSION_ID, SP_DEFAULT_VALUE) + "\n");
		s.append("_id = " + sp.getLong(SP_WORKOUT_SESSION_INDEX, SP_DEFAULT_VALUE) + "\n");
		s.append("userId = " + sp.getLong(SP_WORKOUT_USER_ID, SP_DEFAULT_VALUE) + "\n");
		s.append("lapNumber = " + sp.getInt(SP_WORKOUT_LAP_NUMBER, SP_DEFAULT_VALUE) + "\n");
		s.append("paused = " + (sp.getBoolean(SP_WORKOUT_PAUSED, false) ? "TRUE" : "FALSE") + "\n");
		
		Log.d(TAG, s.toString());
	}
	
}
